#include "GLTFParser.h"
#include "platform/PlatUtil.h"
#include "core/CImage.h"
#include "core/CUtil.h"
#include "core/CElapsedTimer.h"
#include "core/CSerialization.h"
#include "DiskCache.h"
#include "Config.h"

CMap<ModelType, int> GLTFParser::s_meshNumMap;
CMap<ModelType, CMap<int, CString>> GLTFParser::s_meshNames;
CMap<ModelType, CMap<int, CVector<Vec3>>> GLTFParser::s_vertices;
CMap<ModelType, CMap<int, CVector<UInt>>> GLTFParser::s_indices;
CMap<ModelType, CMap<int, CVector<Vec2>>> GLTFParser::s_texcoords;
CMap<ModelType, CMap<int, CVector<Vec3>>> GLTFParser::s_normals;
CMap<ModelType, CMap<int, Mat>> GLTFParser::s_transforms;
CMap<ModelType, CMap<int, PbrMaterial>> GLTFParser::s_materials;
CMap<ModelType, GltfInfo> GLTFParser::s_gltfInfoMap;

std::mutex GLTFParser::s_parsingMutex;
GltfInfo GLTFParser::s_zhicheInfo;
bool GLTFParser::s_isZhicheParsedDone = false;

/**@note use `thread_local` to ensure that each thread accesses its own copy, rather than shared global variables.*/
thread_local int t_meshIndex;
thread_local float t_minX, t_minY, t_minZ, t_maxX, t_maxY, t_maxZ;
void resetRuler() {
	t_meshIndex = 0;
	t_minX = t_minY = t_minZ = std::numeric_limits<float>::max();
	t_maxX = t_maxY = t_maxZ = std::numeric_limits<float>::lowest();/**@note not min()! */
}

/** @warning for CMap map; if using subscripts and no value, a default key-value will be inserted into the map! */
void GLTFParser::parseObj(ModelType type) {
	std::lock_guard<std::mutex> lock(s_parsingMutex);

	LogI << "parsing Model(" << Enum_toStr(type) << ") ......";
	static CElapsedTimer timer;
	timer.reset();

	if (Proj::isSSQK()) {
		createTextureData(type);
		if (ModelType::Zhiche == type)
			s_isZhicheParsedDone = true;
		LogI << "GLTFParser::" << Enum_toStr(type) << " model(*.png) has cached, [took " << timer.elapsed() << "ms]";
		return;
	}

	tinygltf::Model model;
	tinygltf::TinyGLTF loader;
	std::string err;
	std::string warn;

	CByteArray bytes;
	PlatUtil::readFileAsBytes(bytes, Config::modelPath(type));

	bool ret = loader.LoadBinaryFromMemory(&model, &err, &warn, bytes.data(), bytes.size());

	if (!warn.empty()) LogW << warn;
	if (!err.empty()) LogE << err;
	if (!ret) {
		LogE << "modelType = " << Enum_toStr(type) << " load error!";
		throw std::runtime_error("Failed to load glTF model!");
	}

	resetRuler();

	for (const auto& scene : model.scenes) {
		for (const auto& nodeIndex : scene.nodes) {
			const auto& node = model.nodes[nodeIndex];
			parseDataFromNode(type, model, node);
		}
	}

	GltfInfo info;
	info.width = std::abs(t_minX - t_maxX);
	info.length = std::abs(t_minZ - t_maxZ);
	info.height = std::abs(t_minY - t_maxY);
	info.maxX = t_maxX;
	info.maxY = t_maxY;
	info.maxZ = t_maxZ;
	s_gltfInfoMap.insert(type, info);

	LogI << "GLTFParser::" << Enum_toStr(type) << " model(*.glb) has cached, [took " << timer.elapsed() << "ms]";
};

/**
 *@brief recursive computation for model nodes
 *@note assume one node has maximum of one mesh, and one mesh has maximum of one primitive!
 */
void GLTFParser::parseDataFromNode(ModelType type, const tinygltf::Model& model, const tinygltf::Node& node, const glm::dmat4& parentTransform, const std::string& parentPath) {
	glm::dmat4 localTransform = glm::dmat4(1.0);// the local transformation matrix of the current node

	if (!node.matrix.empty())// if the node has a `matrix` attribute, use it directly
		localTransform = glm::make_mat4(node.matrix.data());
	else {// otherwise, combine translation, rotation, scale
		if (!node.translation.empty())
			localTransform = glm::translate(localTransform, glm::make_vec3(node.translation.data()));
		if (!node.rotation.empty()) {
			glm::dquat q = glm::make_quat(node.rotation.data());
			localTransform *= glm::mat4_cast(q);
		}
		if (!node.scale.empty())
			localTransform = glm::scale(localTransform, glm::make_vec3(node.scale.data()));
	}

	// the world transformation matrix of the current node = the world transformation matrix of the parent node * the local transformation matrix of the current node
	glm::dmat4 worldTransform = parentTransform * localTransform;
	s_transforms[type].insert(t_meshIndex, Mat(worldTransform));

	std::string currentPath = parentPath.empty() ? node.name : parentPath + " -> " + node.name;

	if (node.mesh >= 0) { // node.mesh is an int, representing the associated mesh index
		const auto& mesh = model.meshes[node.mesh];
		/*if (type != ModelType::Zhiche) {
			std::string fullMeshName = currentPath + " -> " + mesh.name;
			std::cout << "Mesh Name: " << fullMeshName << std::endl;
			std::cout << "World Transform Matrix (Row-Major):\n";
			std::cout << std::fixed << std::setprecision(5);
			for (int i = 0; i < 4; ++i) {
				std::cout << "| ";
				for (int j = 0; j < 4; ++j) {
					std::cout << std::setw(10) << worldTransform[j][i] << " ";
				}
				std::cout << "|\n";
			}
			std::cout << "modelType = " << static_cast<int>(type) << " ----------------------------------------" << std::endl;
		}*/
		for (const auto& primitive : mesh.primitives) {// traverse each primitive(a geometric part of a mesh)
			// Vertex
			if (primitive.attributes.find("POSITION") != primitive.attributes.end()) {
				CVector<Vec3> _vertices;
				getAccessorData(_vertices, model, primitive.attributes.at("POSITION"));
				s_vertices[type].insert(t_meshIndex, _vertices);

				for (const auto& position : _vertices) {
					auto finalPos = Mat3(worldTransform) * position;
					t_minX = std::min(t_minX, finalPos.x);
					t_minY = std::min(t_minY, finalPos.y);
					t_minZ = std::min(t_minZ, finalPos.z);
					t_maxX = std::max(t_maxX, finalPos.x);
					t_maxY = std::max(t_maxY, finalPos.y);
					t_maxZ = std::max(t_maxZ, finalPos.z);
				}
			}

			// Index
			if (primitive.indices >= 0) {
				CVector<GLuint> _indices;
				getAccessorData<GLuint>(_indices, model, primitive.indices);
				s_indices[type].insert(t_meshIndex, _indices);
			}

			// Normal
			if (primitive.attributes.find("NORMAL") != primitive.attributes.end()) {
				CVector<Vec3> _normals;
				getAccessorData(_normals, model, primitive.attributes.at("NORMAL"));
				s_normals[type].insert(t_meshIndex, _normals);
			}

			// Texture Coord
			if (primitive.attributes.find("TEXCOORD_0") != primitive.attributes.end()) {
				CVector<Vec2> _texcoords;
				getAccessorData(_texcoords, model, primitive.attributes.at("TEXCOORD_0"));
				s_texcoords[type].insert(t_meshIndex, _texcoords);
			}

			// PbrMaterial
			if (primitive.material >= 0) {
				PbrMaterial _material;
				const auto& pbrMaterial = model.materials[primitive.material].pbrMetallicRoughness;
				const auto& baseColorFactor = pbrMaterial.baseColorFactor;// pure color
				if (baseColorFactor.size() == 4) {// #PbrMaterial (if size is not 4, it means bad datas in glTF file)
					float r = baseColorFactor[0], g = baseColorFactor[1], b = baseColorFactor[2], a = baseColorFactor[3];
					auto baseColor = CColor::fromRgbF(r, g, b, a);
					baseColor.eraseAlphaChannel();
					_material.baseColor = baseColor;
				}
				if (pbrMaterial.metallicFactor != -1)// [0, 1]
					_material.metallic = pbrMaterial.metallicFactor;
				else
					_material.metallic = 0;// no metallic

				if (pbrMaterial.roughnessFactor != -1)// [0, 1]
					_material.roughness = pbrMaterial.roughnessFactor;
				else
					_material.roughness = 0;// no roughness

				if (Config::Material_ForceUseCfgValue && ModelType::Zhiche != type) {
					if (ModelType::Cone == type || ModelType::Barrier == type)
						_material.baseColor = "Tomato";
					else
						_material.baseColor = Config::Material_BaseColor;
					_material.metallic = Config::Material_Metallic;
					_material.roughness = Config::Material_Roughness;
				}

				if (pbrMaterial.baseColorTexture.index >= 0) {// has texture image
					int textureIndex = pbrMaterial.baseColorTexture.index;
					const tinygltf::Texture& texture = model.textures[textureIndex];
					const tinygltf::Image& image = model.images[texture.source];
					CByteArray imgBytes = image.image;
					if (imgBytes.empty())
						LogEE << "texture image data is empty!";
					auto texImg = CImage(imgBytes, image.width, image.height, image.component);
					_material.textureImage = texImg;
				}

				s_materials[type].insert(t_meshIndex, _material);
			}

			s_meshNumMap[type] = model.meshes.size();
			s_meshNames[type].insert(t_meshIndex, mesh.name);
			++t_meshIndex;
		}
	}

	// recursive processing of child nodes
	for (const auto& childIndex : node.children) {
		const auto& childNode = model.nodes[childIndex];
		// recursive computation of the world transformation matrix for child nodes
		parseDataFromNode(type, model, childNode, worldTransform, currentPath);
	}
};

/* ************************************************************************************************/
/* ************************************************************************************************/

/**
 * @note at a new thread to avoid blocking other received models' shown.
 * Note: because vector/map is not thread-safe,
 *       be sure to add thread-locker to avoid muti-thread-issue when modifying the global static vector/map at same time!
 */
void GLTFParser::startAsyncParsingZhiche(const std::function<void()>& callback, int delayMs) {
	CUtil::delay([=] {
		parseObj(ModelType::Zhiche);

		s_isZhicheParsedDone = true;
		const auto &info = s_gltfInfoMap[ModelType::Zhiche];
		DiskCache::updateZhicheInfo({info.width, info.length, info.height, info.maxX, info.maxY, info.maxZ});

		callback();
	}, delayMs);
}

const GltfInfo& GLTFParser::zhicheInfo() {
	if (s_isZhicheParsedDone)
		return info(ModelType::Zhiche);

	const auto& zhicheInfo = DiskCache::getZhicheInfo();
	if (zhicheInfo.isValid()) {
		s_zhicheInfo.width = zhicheInfo.width;
		s_zhicheInfo.length = zhicheInfo.length;
		s_zhicheInfo.height = zhicheInfo.height;
		s_zhicheInfo.maxX = zhicheInfo.maxX;
		s_zhicheInfo.maxY = zhicheInfo.maxY;
		s_zhicheInfo.maxZ = zhicheInfo.maxZ;
	}
	return s_zhicheInfo;
}

/**
 *0 - 1
 *|   |
 *2 - 3
 */
void GLTFParser::createTextureData(ModelType type) {
	auto typeName = Enum_toStr(type);
	const auto modelPngPath = Config::Model_Dir + "/2d/" + typeName + ".png";
	PbrMaterial material;
	material.baseColor = Config::Material_BaseColor;
	material.metallic = Config::Material_Metallic;
	material.roughness = Config::Material_Roughness;

	CByteArray bytes;
	PlatUtil::readFileAsBytes(bytes, modelPngPath);
	auto texImg = CImage(bytes);
	texImg.invertY();
	material.textureImage = texImg;

	/*if (std::strstr(typeName, Enum_toStr(ModelType::Car))) material.setTextureUnit(2);
	else if (std::strstr(typeName, Enum_toStr(ModelType::Bus))) material.setTextureUnit(3);
	else if (std::strstr(typeName, Enum_toStr(ModelType::Truck))) material.setTextureUnit(4);
	else if (std::strstr(typeName, Enum_toStr(ModelType::Moto))) material.setTextureUnit(5);
	else if (std::strstr(typeName, Enum_toStr(ModelType::Bike))) material.setTextureUnit(6);
	else if (std::strstr(typeName, Enum_toStr(ModelType::Person))) material.setTextureUnit(7);
	else if (std::strstr(typeName, Enum_toStr(ModelType::Tricycle))) material.setTextureUnit(8);*/

	s_materials[type].insert(0, material);

	CVector<Vec3> vertices;
	const float W = texImg.width() * Config::Model_Scale;
	const float H = texImg.height() * Config::Model_Scale;
	const float Y = Config::Y_Model;

	/**               (Zhiche) 0
	* LT   |    RT        -----|-----> +X
	*      |0             LT   |    RT
	* -----|-----> +X          |
	*      |                   |
	* LB  \|/+Z RB        LB  \|/+Z RB
	*/
	vertices << Vec3(-W/2, Y, -H/2) // left-top
		     << Vec3(W/2, Y, -H/2)  // right-top
		     << Vec3(-W/2, Y, H/2)  // left-bottom
		     << Vec3(W/2, Y, H/2);  // right-bottom

	if (ModelType::Zhiche == type) {// make Zhiche's front is (0,0)
		for (auto& pos : vertices)
			pos.z += H/2;
	}

	s_vertices[type].insert(0, vertices);

	s_meshNumMap.insert(type, 1);
	s_meshNames[type].insert(0, typeName);
	
	CVector<UInt> indices;
	indices << 0 << 2 << 1 << 1 << 2 << 3;
	s_indices[type].insert(0, indices);
	CVector<Vec2> texcoords;
	texcoords << Vec2(0,1) << Vec2(1,1) << Vec2(0,0) << Vec2(1,0);
	s_texcoords[type].insert(0, texcoords);

	GltfInfo info;
	info.width = W;
	info.length = H;
	info.height = 0;
	info.maxX = W/2;
	info.maxY = 0;
	info.maxZ = H/2;
	s_gltfInfoMap.insert(type, info);
}